package com.libservice.screenshotservice

import android.content.ContentResolver
import android.content.Context
import android.content.ContextWrapper
import android.database.Cursor
import android.graphics.BitmapFactory
import android.graphics.Point
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.text.TextUtils
import android.util.Log
import com.libbase.utils.FileUtils
import com.libui.utils.WindowUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Locale

object ScreenshotEventDispatcher {

    private var screenRealSize: Point? = null

    private var latestFileUrl: String? = null

    /**
     * ### Handle media content change (get 1st data and determine whether it is a screenshot file)
     * @param contentUri Uri
     * @param context Context?
     * @param startListenTime Long
     */
    fun handleMediaContentChange(
        contentUri: Uri,
        context: Context?,
        startListenTime: Long,
        screenShotListener: ScreenshotListener?
    ) {
        CoroutineScope(Dispatchers.IO).launch {
            if (context == null) {
                return@launch
            }

            if (screenRealSize == null) screenRealSize =
                WindowUtils.instance.getRealScreenSize(context)

            val cursor: Cursor? = getContentResolverCursor(contentUri, context)
            if (cursor == null || !cursor.moveToFirst()) {
                return@launch
            }

            // Get all of colum index
            with(cursor) {
                val dataIndex = getColumnIndex(MediaStore.Images.ImageColumns.DATA)
                val dateAddedIndex = getColumnIndex(MediaStore.Images.ImageColumns.DATE_ADDED)
                val widthIndex = getColumnIndex(MediaStore.Images.ImageColumns.WIDTH)
                val heightIndex = getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT)

                // Handle media row data
                // File path
                val filePath = cursor.getScreenShotFilePath(dataIndex)

                Log.i("ScreenShotService", "handleMediaContentChange: path $filePath")

                if (!isFilePathLegal(filePath)) {
                    return@with
                }

                // File Date Added
                val dateAdded = cursor.getScreenShotFileDateAdded(dateAddedIndex)
                if (!isFileCreationTimeLegal(dateAdded, startListenTime)) {
                    return@with
                }

                // File Size
                val (width, height) = cursor.getScreenShotFileSize(
                    filePath,
                    widthIndex,
                    heightIndex
                )
                if (!isFileSizeLegal(width, height)) {
                    return@with
                }

                handleScreenShot(context, filePath, screenShotListener)
            }
            if (!cursor.isClosed) cursor.close()
        }
    }

    /**
     * ### Handle screenshot
     * @param filePath String
     */
    private fun handleScreenShot(context: Context, filePath: String, screenShotListener: ScreenshotListener?) {
        CoroutineScope(Dispatchers.Main).launch {
            if (filePath == latestFileUrl) {
                return@launch
            }

            latestFileUrl = filePath

            Log.i("ScreenShotService", "handleScreenShot: path $filePath")

            FileUtils.instance.getCacheDir(ContextWrapper(context))?.let { cacheDir ->
                Log.i("ScreenShotService", "handleScreenShot: cacheDir $cacheDir")

                val cacheFilePath = "$cacheDir/screenshot.png"

                FileUtils.instance.copyFile(filePath, cacheFilePath)

                screenShotListener?.onShot(cacheFilePath)
            }
        }
    }

    /**
     * ### Get screenshot file path
     * @receiver [Cursor]
     * @return [String]
     */
    private fun Cursor.getScreenShotFilePath(dataIndex: Int): String =
        this.getString(dataIndex) ?: ""

    /**
     * ### Get screenshot file date added
     * @receiver [Cursor]
     * @return [Long]
     */
    private fun Cursor.getScreenShotFileDateAdded(dateAddedIndex: Int): Long =
        this.getLong(dateAddedIndex) * 1000

    /**
     * ### Get screenshot file size
     * @receiver [Cursor]
     * @return [Pair]
     */
    private fun Cursor.getScreenShotFileSize(
        filePath: String,
        widthIndex: Int,
        heightIndex: Int
    ): Pair<Int, Int> =
        if (widthIndex >= 0 && heightIndex >= 0) {
            Pair(this.getInt(widthIndex), this.getInt(heightIndex))
        } else {
            // Before API 16, the width and height need to be obtained manually.
            val size = getImageSize(filePath)
            Pair(size.x, size.y)
        }

    /**
     * ### Determine the file path
     * @param filePath String
     * @return Boolean
     */
    private fun isFilePathLegal(filePath: String?): Boolean {
        // File path is not empty
        if (filePath == null || TextUtils.isEmpty(filePath)) {
            Log.w("ScreenShotService", "error: path $filePath")
            return false
        }

        // File path contains screenshot KEYWORDS
        var hasValidScreenShot = false
        val lowerPath = filePath.lowercase(Locale.getDefault())
        for (keyWork: String in KEYWORDS) {
            if (lowerPath.contains(keyWork)) {
                hasValidScreenShot = true
                break
            }
        }
        return hasValidScreenShot
    }

    /**
     * ### Determine the file creation time
     * If the time added to the database is before the start of listening or the difference is greater than 10 seconds from the current time, the screenshot file is considered not the current file.
     * @param dateAdded Long
     * @param startListenTime Long
     * @return Boolean
     */
    private fun isFileCreationTimeLegal(dateAdded: Long?, startListenTime: Long?) =
        if (dateAdded == null
            || startListenTime == null
            || dateAdded / 1000 < startListenTime / 1000
            || (System.currentTimeMillis() - dateAdded) > MAX_COST_TIME
        ) {
            Log.w(
                "ScreenShotService", "error: time doesn't match\n" +
                        "  dateAdded: $dateAdded \n" +
                        "  startListenTime: $startListenTime \n"
            )
            false
        } else true

    /**
     * ### Determine the file size.
     * If the image size exceeds the screen, it is considered that the current screenshot file is not a local screenshot.
     * @param width Int
     * @param height Int
     * @return Boolean
     */
    private fun isFileSizeLegal(width: Int?, height: Int?) =
        screenRealSize?.let {
            if (width == null || height == null) {
                false
            } else if (!((width <= it.x && height <= it.y) || (height <= it.x && width <= it.y))) {
                Log.w(
                    "ScreenShotService", "error: size doesn't match\n" +
                            "  width: $width \n" +
                            "  height: $height \n" +
                            "  screenRealSize: ${it.x} x ${it.y} \n"
                )
                false
            } else {
                true
            }
        } ?: false

    /**
     * ### Get image size
     * @param imagePath String
     * @return [Point]
     */
    private fun getImageSize(imagePath: String): Point {
        val options = BitmapFactory.Options()
        options.inJustDecodeBounds = true
        BitmapFactory.decodeFile(imagePath, options)
        return Point(options.outWidth, options.outHeight)
    }

    /**
     * ### Get content cursor
     * @param contentUri Uri
     * @param context Context
     * @param maxCount Int
     * @return [Cursor]
     */
    private fun getContentResolverCursor(
        contentUri: Uri,
        context: Context,
        maxCount: Int = 1
    ) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        val bundle = Bundle().apply {
            putStringArray(
                ContentResolver.QUERY_ARG_SORT_COLUMNS,
                arrayOf(MediaStore.Images.ImageColumns.DATE_MODIFIED)
            )
            putInt(
                ContentResolver.QUERY_ARG_SORT_DIRECTION,
                ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
            )
            putInt(ContentResolver.QUERY_ARG_LIMIT, maxCount)
        }
        context.contentResolver.query(
            contentUri,
            MEDIA_PROJECTIONS_API_16,
            bundle,
            null,
        )
    } else {
        context.contentResolver.query(
            contentUri,
            MEDIA_PROJECTIONS,
            null,
            null,
            "${MediaStore.Images.ImageColumns.DATE_MODIFIED} desc limit $maxCount",
        )
    }

    private const val MAX_COST_TIME = 10 * 1000
    private val MEDIA_PROJECTIONS = arrayOf(
        MediaStore.Images.ImageColumns.DATA,
        MediaStore.Images.ImageColumns.DATE_ADDED,
    )
    private val MEDIA_PROJECTIONS_API_16 = arrayOf(
        MediaStore.Images.ImageColumns.DATA,
        MediaStore.Images.ImageColumns.DATE_ADDED,
        MediaStore.Images.ImageColumns.WIDTH,
        MediaStore.Images.ImageColumns.HEIGHT,
    )

    @Suppress("SpellCheckingInspection")
    private val KEYWORDS = arrayOf(
        "screenshot", "screen_shot", "screen-shot", "screen shot",
        "screencapture", "screen_capture", "screen-capture", "screen capture",
        "screencap", "screen_cap", "screen-cap", "screen cap",
    )
}